package com.shawn.spring.boot.jdbc.dao;

import com.shawn.spring.boot.jdbc.annotation.Column;
import com.shawn.spring.boot.jdbc.annotation.Id;
import com.shawn.spring.boot.jdbc.annotation.Table;
import com.shawn.spring.boot.jdbc.util.SqlUtil;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.CollectionUtils;

/**
 * @author shawn
 * @since 2020/8/29
 */
public abstract class CommonDao<T, P> {

  private JdbcTemplate jdbcTemplate;
  private Class<T> clazz;

  @SuppressWarnings("unchecked")
  public CommonDao(JdbcTemplate template) {
    this.jdbcTemplate = template;
    clazz =
        (Class<T>)
            ParameterizedType.class.cast(getClass().getGenericSuperclass())
                .getActualTypeArguments()[0];
  }

  public Integer insert(T t) {
    String table = getTableName(t);
    List<Field> filterField = getFields(t, true);
    List<String> columnList = getColumns(filterField);
    String columns = SqlUtil.join(",", columnList);
    String params = SqlUtil.repeatAndJoin("?", ",", columnList.size());
    Object[] values = filterField.stream().map(field -> getFieldValue(field, t)).toArray();
    String sql = String.format("INSERT INTO %s (%s) values (%s)", table, columns, params);
    return jdbcTemplate.update(sql, values);
  }

  public Integer update(T t) {
    String table = getTableName(t);
    List<Field> filterField = getFields(t, true);
    List<String> columnList = getColumns(filterField);
    List<String> cols =
        columnList.stream().map(col -> col = col + " = ?").collect(Collectors.toList());
    String columns = SqlUtil.join(",", cols);
    String primaryKey = getPrimaryKey();
    List<Object> valueList =
        filterField.stream().map(field -> getFieldValue(field, t)).collect(Collectors.toList());
    valueList.add(getPrimaryKeyValue(t));
    String sql = String.format("UPDATE %s set %s where %s = ?", table, columns, primaryKey);
    return jdbcTemplate.update(sql, valueList.toArray());
  }

  public Integer delete(P p) {
    String table = getTableName();
    String primaryKey = getPrimaryKey();
    String sql = String.format("delete from  %s where %s = ?", table, primaryKey);
    return jdbcTemplate.update(sql, new Object[] {p});
  }

  public T selectByPrimaryKey(P p) {
    String table = getTableName();
    String primaryKey = getPrimaryKey();
    String sql = String.format("select * from %s where %s = ?", table, primaryKey);
    RowMapper<T> rowMapper = new BeanPropertyRowMapper<>(clazz);
    List<T> result = jdbcTemplate.query(sql, new Object[] {p}, rowMapper);
    if (CollectionUtils.isEmpty(result)) {
      return null;
    }
    return result.get(0);
  }

  public List<T> selectAll() {
    String table = getTableName();
    String sql = String.format("select * from %s", table);
    RowMapper<T> rowMapper = new BeanPropertyRowMapper<>(clazz);
    return jdbcTemplate.query(sql, rowMapper);
  }

  public List<T> selectByExample(T t) {
    String table = getTableName();
    List<Field> filterField = getFields(t, true);
    List<String> columnList = getColumns(filterField);
    List<String> cols =
        columnList.stream().map(col -> col = col + " = ?").collect(Collectors.toList());
    String columns = SqlUtil.join(" and ", cols);
    Object[] values = filterField.stream().map(field -> getFieldValue(field, t)).toArray();
    String sql = String.format("select * from %s where %s", table, columns);
    RowMapper<T> rowMapper = new BeanPropertyRowMapper<>(clazz);
    return jdbcTemplate.query(sql, values, rowMapper);
  }

  public Integer count() {
    String sql = String.format("select count(*) from %s", getTableName());
    return jdbcTemplate.queryForObject(sql, Integer.class);
  }

  protected String getTableName() {
    Table table = clazz.getAnnotation(Table.class);
    if (Objects.isNull(table)) {
      return clazz.getSimpleName().toLowerCase();
    } else {
      return table.name();
    }
  }

  protected String getTableName(T t) {
    Table table = t.getClass().getAnnotation(Table.class);
    if (Objects.isNull(table)) {
      return t.getClass().getSimpleName().toLowerCase();
    } else {
      return table.name();
    }
  }

  protected List<String> getColumns(List<Field> fieldList) {
    List<String> cols = new ArrayList<>();
    for (Field field : fieldList) {
      field.setAccessible(true);
      Column column = field.getAnnotation(Column.class);
      if (Objects.isNull(column)) {
        cols.add(field.getName());
      } else {
        cols.add(column.name());
      }
    }
    return cols;
  }

  protected List<Field> getFields(T t, boolean ignoreEmpty) {
    Field[] fields = t.getClass().getDeclaredFields();
    return Arrays.stream(fields)
        .filter(
            field ->
                Objects.isNull(field.getAnnotation(Id.class))
                    && (!ignoreEmpty || !Objects.isNull(getFieldValue(field, t))))
        .collect(Collectors.toList());
  }

  protected Object getFieldValue(Field field, T t) {
    try {
      field.setAccessible(true);
      Object o = field.get(t);
      return o;
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
    return null;
  }

  protected String getPrimaryKey(T t) {
    Field[] fields = t.getClass().getDeclaredFields();
    for (Field field : fields) {
      field.setAccessible(true);
      Id id = field.getAnnotation(Id.class);
      if (!Objects.isNull(id)) {
        return id.name();
      }
    }
    return null;
  }

  protected String getPrimaryKey() {
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
      field.setAccessible(true);
      Id id = field.getAnnotation(Id.class);
      if (!Objects.isNull(id)) {
        return id.name();
      }
    }
    return null;
  }

  public Object getPrimaryKeyValue(T t) {
    Field[] fields = t.getClass().getDeclaredFields();
    try {
      for (Field field : fields) {
        field.setAccessible(true);
        Id id = field.getAnnotation(Id.class);
        if (!Objects.isNull(id)) {
          return field.get(t);
        }
      }
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
    return null;
  }

  public Object getPrimaryKeyValue() {
    Field[] fields = clazz.getDeclaredFields();
    try {
      for (Field field : fields) {
        field.setAccessible(true);
        Id id = field.getAnnotation(Id.class);
        if (!Objects.isNull(id)) {
          return field.get(clazz.newInstance());
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }
}
